/*
 * Matt Adams
 * TCSS 558 HW1
 * Fall 2013
 */
package server;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Iterator;
import java.util.SortedSet;

import tools.Log;

/**
 * Handles UDP server functionality in client-server spelling program.
 * 
 * @author Matt Adams
 */
public class UDPSpellingServer extends SpellingServer {
    
    //Static variables
    private static final int MAX_CLOSE_WORDS = 255;
    private static final Charset CHARSET = Charset.forName("US-ASCII");
    
    /**
     * Socket for data transfer.
     */
    private DatagramSocket socket;
    
    /**
     * Constructs UDPSpellingServer object.
     * 
     * @param portFilenameArray Listing of port and filename.
     */
    public UDPSpellingServer(String[] portFilenameArray) {        
        super(portFilenameArray);
        setupSocket();
    }
    
    /**
     * Sets up socket.
     */
    private void setupSocket() {
        try {          
            this.socket = new DatagramSocket(this.port);
            Log.out("Socket created with port: " + this.socket.getLocalPort());
        } catch (SocketException e) {
            Log.err("Error creating socket for udp spelling server: " + e);
            errorClose();
        }
    }
    
    /**
     * Receives data from a client and sends a response back to the same client.
     */
    protected void receiveAndSend() {
        final byte[] queriedWordBytes = new byte[512];
        DatagramPacket receivePacket = new DatagramPacket(queriedWordBytes, queriedWordBytes.length);      
        try {
            this.socket.receive(receivePacket);
        } catch (IOException e) {
            Log.err("IOException when server received datagram packet: " + e);
        }
        final byte[] receivedData = receivePacket.getData();
       
        final boolean validQuery = checkForMalformedQuery(receivedData, receivePacket.getLength());
        if (validQuery) {
            send(receivedData, receivePacket);
        } else {
            Log.err("Invalid query received from: <" + receivePacket.getAddress().getHostAddress() + ">:<" + receivePacket.getPort() + ">");
        }
    }
    
    /**
     * Helper method to send data back to the client.
     * 
     * @param receivedData Received data from client.
     * @param receivePacket Packet received from client.
     */
    protected void send(final byte[] receivedData, final DatagramPacket receivePacket) {
        final byte[] queriedWordBytes = Arrays.copyOfRange(receivedData, 0, receivePacket.getLength() - 1);
        final String queriedWord = new String(queriedWordBytes, CHARSET);
        Log.out("Received query for word: " + queriedWord + ". From: <" + receivePacket.getAddress().getHostAddress() + ">:<" + receivePacket.getPort() + ">");
        
        final DatagramPacket serverResponsePacket = createResponsePacket(queriedWord, receivePacket);
        try {
            this.socket.send(serverResponsePacket);
            Log.out("Response sent to " + "<" + receivePacket.getAddress().getHostAddress() + ">:<" + receivePacket.getPort() + ">");
        } catch (IOException e) {
            Log.err("Error sending datagram packet from server: " + e);
        }       
    }
    
    /**
     * Creates and returns a DatagramPacket containing the server's response to a client query.
     * 
     * @param queriedWord Queried word.
     * @param receivePacket Packet received from client.
     * @return Returns packet containing server's response.
     */
    private DatagramPacket createResponsePacket(final String queriedWord, final DatagramPacket receivePacket) {
        final byte[] serverResponseBytes = new byte[32000];
        DatagramPacket result = null;
        SortedSet<String> closeWords = null;
        final boolean correctWord = this.wordList.isInList(queriedWord);
        if (!correctWord) {
            closeWords = this.wordList.getCloseWords(queriedWord);
        }
        final byte[] receivedData = receivePacket.getData();
        final int dataLength = receivePacket.getLength();
        int i = 0;
        while (i < dataLength) {
            serverResponseBytes[i] = receivedData[i];
            i += 1;
        }
        if (correctWord) {
            result = new DatagramPacket(serverResponseBytes, dataLength + 2, receivePacket.getAddress(), receivePacket.getPort());
            displayQueryResponse(queriedWord, -1, null);
        } else {
            byte signedByte = -1;
            byte unsignedByte = 0;
            if (closeWords.size() > MAX_CLOSE_WORDS) {
                unsignedByte = (byte) (signedByte & (0xFF));
            } else {
                unsignedByte = (byte) (signedByte & closeWords.size());
            } 
            serverResponseBytes[i] = unsignedByte;
            final Iterator<String> itr = closeWords.iterator();
            i += 1;
            int closeWordCount = 0;
            while (itr.hasNext()) {
                final String closeWord = itr.next();
                final byte[] closeWordBytes = closeWord.getBytes(CHARSET);
                if (i + closeWordBytes.length < serverResponseBytes.length) {
                    closeWordCount++;
                    for (int j = 0; j < closeWordBytes.length; j++) {

                        serverResponseBytes[i] = closeWordBytes[j];
                        i += 1;
                    }
                    serverResponseBytes[i] = 0;
                    i+=1;
                }
            }
            result = new DatagramPacket(serverResponseBytes, i, receivePacket.getAddress(), receivePacket.getPort());
            displayQueryResponse(queriedWord, closeWordCount, closeWords);
        }       
        return result;
    }
    
    /**
     * Checks for malformed queries.
     * 
     * @param receivedData Received data from client.
     * @param dataLength Data length.
     * @return Returns whether or not query is valid.
     */
    private boolean checkForMalformedQuery(final byte[] receivedData, final int dataLength) {
        boolean validQuery = true;

        if (dataLength < 1) {
            Log.err("Malformed query: zero bytes.");
            validQuery = false;
        } else if (receivedData[dataLength - 1] != 0) {
            Log.err("Malformed query: not null terminated");
            validQuery = false;
        }

        int i = 0;
        if (i < dataLength - 2 && validQuery) {
            if (receivedData[i] == 0) {
                validQuery = false;
                Log.err("Malformed query: more than 1 null character.");
            }
        }
        return validQuery;
    }

    /**
     * Main to run server.
     * @param portFilenameArray Listing of port and filename.
     */
    public static void main(String[] portFilenameArray) {
        final UDPSpellingServer udpSpellingServer = new UDPSpellingServer(portFilenameArray);
        udpSpellingServer.runServer();
    }

}
